/*
 * Licensed to Nanjing RedOrange Co. ltd (RedOrange) under one or more
 * contributor license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * RedOrange licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */
package redora.rdo.impl;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.mysql.jdbc.JDBC4Connection;
import com.mysql.jdbc.JDBC4PreparedStatement;

import redora.rdo.exceptions.ConnectException;
import redora.rdo.exceptions.QueryException;

/**
 * Base class for the java service entry classes providing the database connection management.
 * This class will cache all prepared statements and it will maintain a database connection.
 * With the makePreparedStatement function a hash in the SQL query determines if a prepared statement
 * is already in the cache.
 * 
 * @author jaap
 */
public abstract class ServiceBase {
	private Logger l = Logger.getLogger("redora.rdo.impl.ServiceBase");

	protected JDBC4Connection con;
	protected JDBC4PreparedStatement ps;
	protected Statement s;
	protected Map<String, JDBC4PreparedStatement> psMap;
	protected ResultSet rs;
	/**
	 * Initializer and connects with the database.
	 * @throws ConnectException When a connection cannot be established.
	 */
	public ServiceBase() throws ConnectException {
		connect();
	}
	
	/** Connects to the database when the is no connect, or the connection was closed */
	public void connect() throws ConnectException {
		if (con == null) {
			l.log(Level.FINE, "Connecting to DB");
			con = DatabaseFactory.connect();
		} else if (con.isClosed()) {
			close();
			l.log(Level.FINE, "Reconnecting to DB");
			con = DatabaseFactory.connect();
		} else {
			l.log(Level.FINE, "reusing connection");
		}
	}

	/**
	 * Closes the database connection and all statements and resultset.
	 * @throws ConnectException
	 */
	public void close() throws ConnectException {
		l.log(Level.FINE, "closing");
		try {
			if (rs != null && !rs.isClosed())
				rs.close();
			rs = null;
			if (s != null && !s.isClosed())
				s.close();
			s = null;
			if (psMap != null)
				for (JDBC4PreparedStatement psClose : psMap.values())
					psClose.close();
			psMap = null;
			if (con != null && !con.isClosed())
				con.close();
			con = null;
		} catch (SQLException e) {
			l.log(Level.SEVERE, "Failed to close connection / statement", e);
			throw new ConnectException(
					"Failed to close connection / statement", e);
		}
	}

	/**
	 * Creates a Prepared Statement on private variable ps. If this statement was already created before
	 * it will retrieve the statement from the cache, otherwise a new PreparedStatement is created.
	 * 
	 * @param sql	SQL statement just like you used in creating a PreparedStatement
	 */
	protected void makePreparedStatement(String sql) throws SQLException, ConnectException {
		l.log(Level.FINE, "Preparing statement");
		connect();
		if (psMap == null) {
			psMap = new HashMap<String, JDBC4PreparedStatement>();
		}
		ps = psMap.get(sql);
		if (ps == null) {
			l.log(Level.FINE, "Creating new statement");
			JDBC4PreparedStatement tempPs = (JDBC4PreparedStatement) con.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
			tempPs.setFetchDirection(ResultSet.FETCH_FORWARD);
			psMap.put(sql, tempPs);
			ps = tempPs;
		}
	}

	/**
	* Looks up the database meta data to determine if the table exists.
	* @return True if the table exists.
	**/
	public boolean tableExists(String tableName) throws ConnectException, QueryException {
		connect();
		boolean retVal = false;
		
		try {
			rs = con.getMetaData().getTables(null, null, tableName, null);
			retVal = rs.next();
		} catch (SQLException e) {
			l.log(Level.SEVERE, "Failed to open meta data to find table " + tableName, e);
			throw new QueryException("Failed to open meta data to find table " + tableName, e);
		}
		
		return retVal;
	}
	
	@Override
	protected void finalize() throws Throwable {
		l.log(Level.FINE, "finalizing");
		close();
		super.finalize();
	}
}
